Repository Public
1import { createReadStream } from "node:fs";2import { stat } from "node:fs/promises";3import { Readable } from "node:stream";4import { sql } from "@/lib/db";5import { currentUser } from "@/lib/auth";6import { MIME, storage, verifySignedUrl } from "@/lib/storage";78/**9 * Media delivery with HTTP range support.10 * Public only while the asset belongs to a published film; otherwise a signed URL or a studio session is required.11 */12const publicCache = new Map<string, { ok: boolean; at: number }>();13const TTL = 20_000; // a takedown stops delivery within 20 seconds1415async function isPublic(sha: string): Promise<boolean> {16 const hit = publicCache.get(sha);17 if (hit && Date.now() - hit.at < TTL) return hit.ok;18 const [r] = await sql<{ ok: boolean }[]>`SELECT bool_or(public) AS ok FROM assets WHERE sha256 = ${sha}`;19 publicCache.set(sha, { ok: !!r?.ok, at: Date.now() });20 if (publicCache.size > 2000) publicCache.clear();21 return !!r?.ok;22}2324export async function GET(req: Request, ctx: { params: Promise<{ file: string }> }) {25 const { file } = await ctx.params;26 const m = /^([a-f0-9]{64})\.(mp4|jpg|png|webp|mp3|vtt)$/.exec(file);27 if (!m) return new Response("Not found", { status: 404 });28 const [, sha, ext] = m;29 const url = new URL(req.url);30 const open = await isPublic(sha);31 if (!open && !verifySignedUrl(sha, ext, url.searchParams.get("exp"), url.searchParams.get("sig")) && !(await currentUser())) {32 return new Response("Not found", { status: 404 });33 }34 const path = storage().pathOf(sha, ext);35 let size: number;36 try { size = (await stat(path)).size; } catch { return new Response("Not found", { status: 404 }); }3738 const headers: Record<string, string> = {39 "Content-Type": MIME[ext] ?? "application/octet-stream",40 "Accept-Ranges": "bytes",41 ETag: `"${sha}"`,42 "Cache-Control": open ? "public, max-age=3600, stale-while-revalidate=86400" : "private, no-store",43 "X-Content-Type-Options": "nosniff",44 };45 if (url.searchParams.get("download") === "1" && open) headers["Content-Disposition"] = `attachment; filename="clip-${sha.slice(0, 10)}.${ext}"`;46 if (req.headers.get("if-none-match") === `"${sha}"`) return new Response(null, { status: 304, headers });4748 const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.get("range") ?? "");49 if (range && (range[1] || range[2])) {50 let start = range[1] ? Number(range[1]) : size - Number(range[2]);51 let end = range[1] && range[2] ? Number(range[2]) : size - 1;52 if (!range[1]) start = Math.max(0, start);53 end = Math.min(end, size - 1);54 if (start > end || start >= size) return new Response(null, { status: 416, headers: { "Content-Range": `bytes */${size}` } });55 return new Response(Readable.toWeb(createReadStream(path, { start, end })) as ReadableStream, {56 status: 206, headers: { ...headers, "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": String(end - start + 1) },57 });58 }59 return new Response(Readable.toWeb(createReadStream(path)) as ReadableStream, { status: 200, headers: { ...headers, "Content-Length": String(size) } });60}61